<?php
namespace x_mier\auth;

use think\facade\Db;
use think\facade\Request;

/**
 * 权限认证类
 * 功能特性：
 * 1，是对规则进行认证，不是对节点进行认证。用户可以把节点当作规则名称实现对节点进行认证。
 *      $auth=new Auth();  $auth->check('规则名称','用户id')
 * 2，可以同时对多条规则进行认证，并设置多条规则的关系（or或者and）
 *      $auth=new Auth();  $auth->check('规则1,规则2','用户id','and')
 *      第三个参数为and时表示，用户需要同时具有规则1和规则2的权限。 当第三个参数为or时，表示用户值需要具备其中一个条件即可。默认为or
 * 3，一个用户可以属于多个用户组(auth_group_access表 定义了用户所属用户组)。我们需要设置每个用户组拥有哪些规则(auth_group 定义了用户组权限)
 *
 * 4，支持规则表达式。
 *      在auth_rule 表中定义一条规则时，如果type为1， condition字段就可以定义规则表达式。 如定义{score}>5  and {score}<100  表示用户的分数在5-100之间时这条规则才会通过。
 */
class Auth
{
    /**
     * Request实例
     * @var \think\Request
     */
    public $request;

    /**
     * 应用实例
     * @var \think\App
     */
    public $app;
    //默认配置
    public $config = [
        'auth_on' => 1, // 权限开关
        'auth_type' => 1, // 认证方式，1为实时认证；2为登录认证。
        'auth_group' => 'system_auth_group', // 用户组数据表名
        'auth_rule' => 'system_auth_rule', // 权限规则表
        'auth_user' => 'system_auth_user', // 用户信息表
        'auth_rule_special' => 'system_auth_rule_special', // 特殊权限表
        'auth_rule_access' => 'system_auth_rule_access', // 权限规则->特殊权限表
    ];
    public function getLogin($username, $password)
    {
        if (empty($username)) {
            return tocode(100, '用户名不能为空！');
        }
        $res = Db::name($this->config['auth_user'])
            ->alias('a')
            ->join($this->config['auth_group'] . ' b ', 'b.id= a.group_id', 'LEFT')
            ->where('user_name', $username)
            ->field('a.*,b.rules')
            ->find();
        if (!$res) {
            $msg = "用户不存在!";
        } elseif ($res['passwd'] != passwd($password)) {
            $msg = "密码错误!" . $password;
        } elseif ($res['state'] == 0) {
            $msg = "禁止登录!";
        }
        $is_push = 0;
        if (!empty(config_group('is_push'))) {
            $is_push = 1;
        }
        if ($is_push) {
            $title = "后台登录通知";
            $time = date('Y/m/d H:i:s');
            $ip = request()->ip();
            $ipaddr = get_ip_area($ip); //根据ip地址获取地域信息
        }
        if (!empty($msg)) {
            $desc = '登陆通知 用户:' . $username . ' 登陆失败，原因：' . $msg;
            writelog($desc, 100, 1, $res['id'], $res['user_name']);
            if ($is_push) {
                $content = <<<EOF
                后台登录通知
                \n
                用户：{$username}于{$time} 在  {$ipaddr} 登陆失败，原因：{$msg}，ip地址  {$ip}
                EOF;
                push($title, $content);
            }
            return tocode(100, $msg);
        }
        /**-------------- 更新登录记录 --------------*/
        Db::name($this->config['auth_user'])->where('id', $res['id'])->update(['last_login_ip' => request()->ip(), 'last_login_time' => time()]);
        /**-------------- 将记录存到session --------------*/
        unset($res['password']);
        if (!empty($res['image'])) {
            $res['image_text'] = _file($res['image']);
        }
        $desc = '登陆通知 用户:' . $username . ' 登陆成功';
        $seal_ip = request()->ip();
        writelog($desc, 200, 1, $res['id'], $res['user_name']);
        if ($is_push) {
            $content = <<<EOF
            后台登录通知[强制退出](https://52xml.cn/api/api/seal_ip?ip={$seal_ip}&sess=1)
            \n
            用户：{$username}于{$time} 在  {$ipaddr} 登陆成功，ip地址  {$ip}
            EOF;
            push($title, $content);
        }
        return $res;
    }
    /**
     * 检查权限
     * @param $name string|array  需要验证的规则列表,支持逗号分隔的权限规则或索引数组
     * @param $uid  int           认证用户的id
     * @param int $type 认证类型
     * @param string $mode 执行check的模式
     * @param string $relation 如果为 'or' 表示满足任一条规则即通过验证;如果为 'and'则表示需满足所有规则才能通过验证
     * return bool               通过验证返回true;失败返回false
     */
    public function auth_check($name, $uid, $type = 1, $mode = 'url', $relation = 'or')
    {
        if (!$this->config['auth_on']) {
            return true;
        }
        $name = strtolower($name);
        // 获取用户需要验证的所有有效规则列表
        $authList = $this->getAuthList($uid, $type);
        if (is_string($name)) {
            $name = strtolower($name);
            if (strpos($name, ',') !== false) {
                $name = explode(',', $name);
            } else {
                $name = [$name];
            }
        }
        $list = []; //保存验证通过的规则名
        if ('url' == $mode) {
            $REQUEST = unserialize(strtolower(serialize(Request::param())));
        }
        foreach ($authList as $auth) {
            $query = preg_replace('/^.+\?/U', '', $auth);
            if ('url' == $mode && $query != $auth) {
                parse_str($query, $param); //解析规则中的param
                $intersect = array_intersect_assoc($REQUEST, $param);
                $auth = preg_replace('/\?.*$/U', '', $auth);
                if (in_array($auth, $name) && $intersect == $param) {
                    //如果节点相符且url参数满足
                    $list[] = $auth;
                }
            } else {
                if (in_array(strtolower($auth), $name)) {
                    $list[] = $auth;
                }
            }
        }
        if ('or' == $relation && !empty($list)) {
            return true;
        }
        $diff = array_diff($name, $list);
        if ('and' == $relation && empty($diff)) {
            return true;
        }
        return false;
    }
    /**
     * 根据用户id获取用户组,返回值为数组
     * @param  $uid int     用户id
     * return array       用户所属的用户组 array(
     *     array('uid'=>'用户id','group_id'=>'用户组id','title'=>'用户组名称','rules'=>'用户组拥有的规则id,多个,号隔开'),
     *     ...)
     */
    public function getGroups($uid)
    {
        static $groups = [];
        if (isset($groups[$uid])) {
            return $groups[$uid];
        }
        // 转换表名
        $auth_group = $this->config['auth_group'];
        $auth_user = $this->config['auth_user'];
        // 执行查询
        $user_groups = Db::name($auth_group)
            ->alias('a')
            ->join($auth_user . ' b', 'b.group_id= a.id')
            ->where('b.id', $uid)
            ->field('a.rules,a.title,b.group_id,b.id as uid')
            ->select();
        $groups[$uid] = $user_groups ?: [];
        return $groups[$uid];
    }
    /**
     * 获得权限列表
     * @param integer $uid 用户id
     * @param integer $type
     * @return array
     */
    public function getAuthList($uid, $type)
    {
        static $_authList = []; //保存用户验证通过的权限列表
        $t = implode(',', (array) $type);
        if (isset($_authList[$uid . $t])) {
            return $_authList[$uid . $t];
        }
        if (session('_auth_list_' . $uid . $t)) {
            return session('_auth_list_' . $uid . $t);
        }
        //读取用户所属用户组
        $groups = $this->getGroups($uid);
        // dd($groups);
        $ids = []; //保存用户所属用户组设置的所有权限规则id
        foreach ($groups as $g) {
            $ids = array_merge($ids, explode(',', trim($g['rules'], ','))); //trim移除字符串两侧的字符,explode把字符串打散为数组。 array_merge把两个数组合并为一个数组
        }
        $ids = array_unique($ids); //移除数组中重复的值
        if (empty($ids)) {
            $_authList[$uid . $t] = [];
            return [];
        }
        // dd($ids);
        $map[] = ['a.id', 'in', $ids];
        $map[] = ['a.type', '=', $type];
        $map[] = ['a.state', '=', 1];
        $map[] = ['b.group_id', '=', $groups[0]['group_id']];
        $auth_rule_access = $this->config['auth_rule_access'];
        //读取用户组所有权限规则
        $rules = Db::name($this->config['auth_rule'])
            ->alias('a')
            ->join($auth_rule_access . ' b', 'b.rule_id = a.id', 'LEFT')
            ->where($map)
            ->field('url,title,b.rule_special')
            ->select();
        //循环规则，判断结果。
        $authList = [];
        $con_url = [];
        $rule_special = [];
        foreach ($rules as $rule) {
            //生成特殊功能
            $co_url = explode('/', $rule['url'])[0];
            if (!empty($rule['rule_special'])) {
                if (!in_array($co_url, $con_url)) {
                    $rule_special = array_merge_recursive($rule_special, array_column($this->get_rule_special('', $rule['rule_special'], $co_url), 'url'));
                }
            }
            $con_url[] = $co_url;
            //只要存在就记录
            if (!empty($rule['url'])) {
                $authList[] = strtolower($rule['url']);
            }
        }
        $authList = array_merge_recursive($authList, $rule_special);

        $_authList[$uid . $t] = $authList;
        //规则列表结果保存到session
        session('_auth_list_' . $uid . $t, $authList);
        return array_unique($authList);
    }
    /**
     * 获得权限列表
     * @param integer $uid 用户id
     * @param integer $type
     * return array
     */
    public function getRule($map = [] || false, $all = false)
    {
        $session = session('auth_user');
        if ($session['group_id'] !== 1) {
            $map[] = ['id', 'in', array_map('intval', explode(',', $session['rules']))];
        }
        if (!$all) {
            $map[] = ['state', '=', 1];
        }
        $rules = Db::name($this->config['auth_rule'])
            ->where($map)
            ->order(['sort' => 'asc', 'id' => 'asc'])
            ->select()->toArray();
        return $rules;
    }
    /**
     * 特殊功能
     */
    public function get_rule_special($id = false, $rule_special = false, $url = false, $all = false)
    {
        $tree = Db::name($this->config['auth_rule_special'])->cache(true, 60)->where('state', 1)->select()->toArray();
        if ($rule_special) {
            foreach ($tree as &$k) {
                if ($rule_special === true) {
                    $k['url'] = $url . '/' . $k['url'];
                } else {
                    if (in_array($k['id'], array_map('intval', explode(',', $rule_special)))) {
                        if ($url) {
                            $k['url'] = $url . '/' . $k['url'];
                        } else {
                            $k['checked'] = true;
                        }
                    }
                    if ($all) {
                        $k['checked'] = true;
                    }
                }
                $k['id'] = $id . '[' . $k['id'];
                $k['pid'] = $id;
            }
            return $tree;
        } else {
            foreach ($tree as &$k) {
                $k['id'] = $id . '[' . $k['id'];
            }
        }
        return $tree;
    }
    /**
     * 获得用户资料,根据自己的情况读取数据库
     */
    public function getUserInfo($uid)
    {
        static $userinfo = [];
        $user = Db::name($this->config['auth_user']);
        // 获取用户表主键
        $_pk = is_string($user->getPk()) ? $user->getPk() : 'uid';
        if (!isset($userinfo[$uid])) {
            $userinfo[$uid] = $user->where($_pk, $uid)->find();
        }
        return $userinfo[$uid];
    }
}
